Chapter 16 / Graceful Shutdown

Graceful Shutdown

Mengajari backend Anda tata krama yang baik: menyelesaikan kerja yang sedang berjalan, membersihkan diri, dan menutup pintu dengan sopan alih-alih membantingnya.


Versi Sederhana

01 / MOTIVATION

Skenarionya

Mari bayangkan skenario yang sangat realistis. Anda sedang di tengah memproses transaksi pembayaran kritis dan tiba-tiba server Anda perlu restart untuk deployment. Seseorang mendorong sesuatu ke lingkungan produksi dan server Anda perlu men-deploy dirinya.

Zero-Downtime Deployment Tidak Menghilangkan Masalah

Tentu, kita punya teknik seperti zero-downtime deployment yang memastikan server yang ada tidak mati sebelum server baru (server dengan kode baru kami) naik dan siap menerima lalu lintas. Mekanisme itu ada.

Tapi ini jebakannya: pada satu titik, saat server baru siap online dan siap menerima lalu lintas, server lama harus dimatikan. Ia harus berhenti menerima lalu lintas, dan transisi akan terjadi ke server baru. Kita bicara momen kritis itu, dan kebetulan Anda sedang di tengah transaksi.

The E-Commerce Payment Question

Anggap Anda sedang di tengah transaksi e-commerce, membeli sesuatu dari Amazon atau Flipkart, dan server Amazon/Flipkart perlu restart untuk deployment. Pertanyaannya jadi: 1. Apa yang persis terjadi pada pembayaran itu? Apakah ia hilang di dunia digital? 2. Apakah pelanggan (Anda) ditagih dua kali karena semacam race condition? Ini skenario yang harus Anda pikirkan sebagai engineer backend.

Ini bukan masalah baru. Ia sudah ada sejak awal server dan backend. Dan tentu kita sudah punya solusinya, solusi itu disebut graceful shutdown, dan persis seperti namanya: kita ingin menghentikan server kita dengan anggun. Kita tidak ingin berhenti mendadak atau tiba-tiba. Itulah seluruh gagasannya.

Why Graceful Shutdown Matters

Memperhatikan graceful shutdown memberi aplikasi Anda pengalaman pengguna yang sangat baik dan menghindari masalah seperti korupsi data. Jika Anda di tengah transaksi pembayaran, ia membuat Anda menghindari: tagihan ganda ke pelanggan, transaksi yang hilang, dan harus memproses refund untuk tagihan yang gagal/duplikat.


02 / DEFINITION

Apa Itu Graceful Shutdown?

We want to teach our backend good manners, it cannot just slam the door when it's time to leave.

Jika kita menyederhanakan berlebihan apa arti graceful shutdown bagi server kita, kita pada dasarnya bisa bilang kita ingin mengajari server kita, mengajari backend kita, tata krama yang baik. Ia tidak bisa berhenti mendadak saat berada di antara transisi ke deployment baru. Ia harus melakukan semacam langkah.

Analogi "Tata Krama yang Baik"

Anggap seperti punya tamu. Saat jam 21:00 dan waktunya tidur, Anda tidak mendorong tamu keluar pintu dan membanting pintu di depan wajah mereka. Ada langkah yang harus Anda lakukan. Begitu pula, backend Anda dengan sopan:

  • Menyelesaikan percakapan yang berjalan: menyelesaikan apa pun yang sedang ia lakukan
  • Mengucapkan selamat tinggal ke semua tamu: menutup interaksi yang ada dengan benar
  • Membersihkan diri: merapikan resource yang ia pakai
  • Lalu akhirnya menutup pintu: baru setelah itu ia benar-benar keluar

Inilah seni dan ilmu membuat aplikasi backend Anda sebaik mungkin berperilaku dan seefisien mungkin. Sisa bab ini membangun fondasinya: kenapa kita melakukan ini, konsep di sekitarnya, dan bagaimana ia sebenarnya diimplementasikan.


03 / FOUNDATIONS

Manajemen Siklus Hidup Proses

Konsep pertama yang harus dipahami. Backend Anda adalah aplikasi yang berjalan sebagai proses di semacam server, di semacam komputer. Ini penting: semua yang berjalan di sistem operasi berjalan sebagai proses.

Jika Anda familiar dengan konsep sistem operasi, ini masuk akal. Jika tidak, bukan masalah besar, cukup pelajari istilahnya: process. Apa pun yang Anda jalankan, ia berjalan di dalam proses.

Setiap Proses Punya Siklus Hidup

Seperti semua makhluk hidup, setiap proses punya siklus hidup: kapan ia mulai, bagaimana ia mulai, kapan ia berakhir, dan bagaimana ia berakhir. Dalam satu cara:

Process lifecycle three phases Born process starts Living process executing Dies process terminated
Fig 1.1: The three phases of a process lifecycle: born -> living -> dies

Mereka lahir saat proses mulai, mereka hidup saat proses mengeksekusi, dan mereka mati saat proses dihentikan. Seluruhnya disebut siklus hidup proses. Memahaminya esensial karena ia sangat erat berhubungan dengan cara graceful shutdown diimplementasikan.

OS Tidak Sekadar Mencabut Colokan

Saat sistem operasi Anda memutuskan sudah waktunya aplikasi Anda berhenti berjalan, ia tidak sekadar mencabut colokan atau mematikan prosesnya. Ia mengikuti protokol komunikasi yang mapan: cara berkomunikasi dengan proses untuk bilang "sudah waktunya kamu berhenti, kita akan mengikuti langkah-langkah ini, lalu kita menghentikannya."

Anda bisa bayangkan seperti percakapan antara sistem operasi dan aplikasi Anda (yang berjalan di dalam proses di OS Anda):

The Conversation (Simplified)

OS: "Hei, sudah waktunya kamu berhenti." Aplikasi: "Oke, beri aku beberapa detik (realistisnya), lalu aku akan menghentikan diri sendiri, atau kamu bisa menghentikanku." Tentu, percakapan ini tidak terjadi lewat teks, kita bicara program, yang tidak memahami teks. Seluruh komunikasi ini terjadi lewat konsep bernama signals.


04 / SIGNALS

Signal & Komunikasi Antar-Proses

Signal adalah konsep penting di sistem operasi Unix. Saat kita bilang Unix, maksudnya semua sistem operasi Linux (Arch, Ubuntu, dll.) dan juga Mac, Mac berasal dari inti Unix, kernel Unix.

Why Linux Matters for Servers

Kebanyakan saat kita bicara server, yang dimaksud hanya Linux, karena 99% waktu, kapan pun Anda men-deploy aplikasi ke penyedia cloud, ia memilih sistem operasi Linux dan men-deploy aplikasi Anda di sana. Anda tidak akan pernah melihat Windows kecuali untuk kasus khusus seperti Windows Server. Untuk men-deploy server, kita dominan memakai sistem operasi berbasis Linux.

Signal Dipakai untuk IPC

Sistem operasi Unix punya konsep bernama signals, yang dipakai untuk IPC (Inter-Process Communication). Sederhananya, IPC adalah teknik yang dipakai dua proses untuk berkomunikasi satu sama lain memakai protokol yang mapan (yang internalnya tidak perlu Anda khawatirkan).

Cara Kerja Handler

Aplikasi Anda berjalan di dalam proses, dan ia mendaftarkan beberapa handler. Apa maksud handler?

Handler pada dasarnya kode yang:

  • Menunggu dan berjalan terus di belakang layar
  • Sedang menunggu semacam komunikasi, semacam signal: dari sistem operasi
  • Mendeteksi kapan pun signal itu datang, lalu melakukan sesuatu (pembersihan, menyelesaikan request, dll.)

Handler ini pada dasarnya memberi tahu sistem operasi Anda: "Saat kamu ingin aku berhenti, kirim aku pesan spesifik ini, dan aku akan menanganinya dengan tepat. Aku akan menghentikan diriku memakai protokol dan langkah yang sudah didefinisikan." Anda tidak bisa sekadar bilang "stop", itu teks yang bisa dibaca manusia. Ia harus signal spesifik.

OS to application signal handler communication Operating System sends signal Process Application your backend Signal Handler waits + reacts SIGTERM handler runs cleanup & finishes work, then process exits
Fig 2.1: The OS sends a signal; the application's registered handler detects it and runs the shutdown steps

Tiga Signal

Awalan SIG di semua ini sederhananya berarti signal. Bagian kedua perintah sebenarnya. Ada tiga signal yang kita pedulikan:

4.1: SIGTERM (Terminate)

SIG berarti signal, TERM berarti terminate. SIGTERM adalah cara sopan sistem operasi Anda meminta aplikasi Anda mati. Ia bukan cara ekstrem, hanya sentuhan.

The Shoulder-Poke Analogy

Bayangkan Anda berdiri dan seseorang datang dari belakang dengan lembut mencolek bahu Anda, "hei, permisi, bisakah kamu menyelesaikan dan pergi?" Itu SIGTERM. Request yang sangat lembut, dan karena itu, backend Anda punya kesempatan menyelesaikan apa pun yang sedang ia lakukan. Ia tidak harus pergi di detik itu juga, ia mendapat jendela beberapa detik.

Apa yang mungkin dilakukan backend Anda saat menerima SIGTERM? Karena kita bicara backend HTTP, ia mungkin memproses request: itu hal utama yang dilakukan backend Anda. Klien Anda (frontend, web app, ekstensi Chrome, apa pun) mengirim request HTTP, dan backend Anda memprosesnya dan mengembalikan respons.

Di titik waktu acak, backend Anda mungkin memproses 10-12 request, atau jika aplikasi Anda cukup besar, ratusan atau bahkan 500-600 request serentak. Saat ia mendapat signal SIGTERM, waktunya ia melakukan tiga langkah ini:

SIGTERM three steps 1. Finish existing requests complete in-flight work 2. Clean up resources DB, files, connections 3. Exit close the door
Fig 2.2: The three SIGTERM steps: finish requests -> clean up -> exit

Siapa yang Memakai SIGTERM?

SIGTERM kebanyakan dipakai sistem deployment, process manager, atau platform orkestrasi: pada dasarnya sistem apa pun yang Anda dirikan untuk mengelola proses Anda:

  • Kubernetes: orkestrasi container
  • systemd: manajer layanan Linux
  • PM2: process manager Node.js

Sistem dan tool ini memakai SIGTERM untuk dengan benar membiarkan aplikasi Anda menyelesaikan apa pun yang sedang ia lakukan, membersihkan, dan pergi dengan anggun.

4.2: SIGINT (Interrupt)

SIG berarti signal, INT berarti interrupt. Kasus pemakaian paling terkenal dari signal ini, yang hampir pasti pernah Anda pakai sebagai developer, adalah Ctrl + C.

Jika Anda pernah bekerja dengan aplikasi command-line atau berbasis terminal, Anda pernah memakainya: ada proses atau tugas berjalan, dan jika ingin menutupnya mendadak, Anda menekan Ctrl + C dan proses itu langsung berhenti.

Live Demo From the Lecture

Di kuliah, backend berbasis Go berjalan lokal, siap menerima request. Menekan Ctrl + C di keyboard mencatat: "a signal has been received and the signal type is an interrupt signal." Karena graceful shutdown diimplementasikan, aplikasi mencatat tiap langkah saat mematikan, alih-alih mati seketika.

SIGINT menuntut user/developer menekan tombol (Ctrl + C memakai keyboard). Jadi ia kebanyakan dipakai di lingkungan pengembangan dan juga disebut shutdown yang diprakarsai pengguna. Dalam komunikasi proses-ke-proses, SIGINT biasanya tidak dipakai, karena ia butuh penekanan tombol.

Tangani SIGINT dengan Cara yang Sama seperti SIGTERM

Di hampir semua kasus, Anda ingin menangani SIGINT dengan cara yang sama seperti menangani SIGTERM. Jika dipikir-pikir, masuk akal:

SignalBiasanya diprakarsai olehContoh konteks
SIGINTManusia (penekanan tombol)Developer menekan Ctrl+C lokal untuk menghentikan dev server
SIGTERMSebuah programPM2 di instance AWS EC2 memberi signal backend Anda yang ter-deploy untuk berhenti

Tidak peduli apakah backend Anda berjalan di lingkungan pengembangan dan Anda menghentikannya dengan Ctrl + C, versus berjalan di dalam instance AWS EC2 yang dikelola PM2 (process manager) yang mengirim SIGTERM. Di kedua kasus, maksudnya sama: kita ingin mati. Dan kita ingin mati dengan cara yang bersih dan anggun. Yang penting maksudnya, bukan apakah manusia atau program yang memprakarsainya.

4.3: SIGKILL (Opsi Nuklir)

SIG berarti signal, KILL perintah sebenarnya, dan persis seperti namanya. Kita ingin membunuh aplikasi seketika.

SIGKILL Cannot Be Caught or Ignored

Hal yang menarik (dan berbahaya) tentang SIGKILL: ia tidak bisa ditangkap dan tidak bisa diabaikan: Tidak bisa ditangkap: Aplikasi Anda tidak bisa mendaftarkan handler yang melakukan pembersihan saat menerima SIGKILL. Aplikasi tidak diberi kemampuan/izin itu untuk mendeteksinya. Tidak bisa diabaikan: Anda tidak bisa bilang "karena aku tidak bisa mendeteksinya, aku akan mengabaikannya dan tidak berhenti." Itu juga tidak terjadi.

Jika aplikasi Anda dikirim SIGKILL, ia tidak akan bisa mendeteksinya, dan ia harus berhenti di detik itu juga. Tidak ada hal lain yang terjadi, ia hanya berhenti. Itulah kenapa disebut kill signal.

The Pull-the-Plug Analogy

Bayangkan perbedaan dua cara mematikan komputer Anda: Anggun (SIGTERM/SIGINT): Mengklik ikon sistem -> mengklik "Shutdown" -> OS menutup aplikasi dengan benar. SIGKILL: Langsung ke colokan listrik dan mencabutnya. Komputer Anda mati begitu saja. Tanpa pembersihan, tanpa pamit.

Kenapa Ini Membuat Graceful Shutdown Penting

Inilah rantai konsekuensi yang kritis. Signal sopan adalah SIGTERM dan SIGINT, ini membuat Anda menyelesaikan apa pun yang sedang Anda lakukan, membersihkan, dan keluar dengan anggun. Jika Anda tidak menghormati signal sopan, maka akhirnya Anda akan menerima SIGKILL. Anda harus berhenti, dan Anda bahkan tidak akan mendapat kesempatan membersihkan diri.

Signal escalation from polite to forced Polite: SIGTERM / SIGINT window to finish + clean up if ignored / timeout exceeded Forced: SIGKILL instant stop, no cleanup
Fig 2.3: Ignore the polite signals and the OS escalates to SIGKILL, which gives you no chance to clean up

Inilah alasan inti graceful shutdown konsep penting: ini kesempatan Anda menangani shutdown sebelum OS turun ke opsi nuklir.


05 / STEP ONE

Connection Draining

Sekarang kita masuk dalam ke dua hal penting yang terjadi selama graceful shutdown: menyelesaikan request yang ada (bagian ini) dan membersihkan resource (bagian berikutnya). Bagian penting pertama menghentikan request yang sedang berjalan.

Apa Itu Request On-the-Fly?

Server HTTP Anda memproses banyak request secara serentak. Saat waktunya menghentikan server Anda, mungkin backend Anda sudah memproses sejumlah request, 10, 12, ratusan, atau ribuan tergantung skala. Request yang sudah diproses di momen itu adalah request on-the-fly (in-flight).

Analogi Restoran

Bayangkan Anda pergi ke restoran bersama teman, dan restoran harus tutup (jam 22:30-23:00, atau karena alasan lain). Apa yang terjadi? Pemiliknya tidak bisa sekadar mematikan semua lampu dan mengusir Anda. Gantinya:

Restaurant closing analogy three steps 1. Stop new customers -> stop accepting new conns 2. Let diners finish -> finish in-flight requests 3. Pay & leave -> close the connection
Fig 3.1: Restaurant closing maps directly onto connection draining
  • Berhenti mengizinkan pelanggan baru: Seseorang di resepsionis/gerbang berhenti membiarkan orang baru masuk. Anda tidak ingin pelanggan baru yang harus Anda tolak.
  • Umumkan ke pelanggan yang ada: Beri tahu semua yang sedang makan: "Sudah waktunya tutup, Anda punya 15-20 menit menyelesaikan makanan. Santai saja." 15-20 menit lebih dari cukup.
  • Bayar dan pergi: Mereka selesai, membayar (dan tip!), dan meninggalkan restoran.

Gagasan yang Sama untuk Backend Anda

Kita menyebut proses ini connection draining. Saat aplikasi Anda menerima signal shutdown (SIGTERM dari proses, atau SIGINT dari Ctrl+C developer), hal pertama yang harus ia lakukan berhenti menerima koneksi baru: persis seperti menghentikan pelanggan baru masuk. Ini mencegah situasi menjadi lebih kacau dan lebih sulit ditangani. Lalu ia membiarkan koneksi yang ada selesai secepat mungkin.

Connection Draining per Arsitektur

Implementasinya berbeda tergantung arsitektur aplikasi, tapi gagasan tingkat tingginya selalu proses tiga langkah yang sama: berhenti menerima baru -> selesaikan yang ada -> tutup koneksi.

ArsitekturApa arti "draining"
Backend HTTPBerhenti menerima request HTTP baru dari klien mana pun; biarkan request in-flight selesai
Database (juga backend!)Selesaikan semua query/transaksi yang ada; berhenti menerima query baru ke eksekusi sebelum menutup koneksi
Koneksi WebSocketPertama beri tahu klien ia akan menutup, lalu tutup socket-nya, jangan pernah menutup mendadak
A Database is a Backend Too

Seperti dibahas di bab sebelumnya, database juga backend, bisa dibayangkan sebagai backend. Bukan dalam arti HTTP, tapi ia tetap aplikasi yang berjalan sebagai proses dan mengikuti prinsip graceful shutdown yang sama: selesaikan transaksi yang ada, berhenti menerima yang baru, lalu tutup.

5.1: Kompromi Timeout

Tantangan connection draining adalah waktu. Anda ingin memberi koneksi yang ada cukup waktu menyelesaikan kerjanya, tapi Anda tidak bisa menunggu selama yang mereka butuhkan. Harus ada batas.

Kebanyakan sistem produksi mengimplementasikan mekanisme timeout: umumnya 30 detik (kadang 60). Ini durasi maksimum sistem Anda akan menunggu. Setelah itu, ia berhenti begitu saja. Kebanyakan waktu, jika Anda tidak menerima request baru, 30 detik lebih dari cukup untuk menyelesaikan semua request yang ada. Tapi jika ada operasi blocking yang tidak bisa selesai dalam jendela itu, Anda akan dihentikan paksa: itu rencana cadangannya. Anda tidak bisa membiarkan backend Anda mengambil waktu sebebasnya; timeout adalah batas keras.

The Design Consideration / How Long Should You Wait?

Terlalu pendek -> Anda berisiko menginterupsi operasi sah yang sebenarnya (pembayaran nyata di tengah jalan terpotong). Terlalu panjang -> seluruh proses shutdown Anda jadi lamban, yang akhirnya memengaruhi kecepatan deployment dan daya tanggap sistem. Tidak ada aturan keras dan cepat. Timeout yang tepat tergantung durasi request khas aplikasi Anda dan kebutuhan operasional Anda. Untuk backend tradisional/normal, 30 detik lebih dari cukup. Untuk WebSocket atau arsitektur lebih rumit, Anda harus memahami sistem Anda dan memilih sesuai.

Koordinasi dengan Load Balancer & Service Discovery

Connection draining juga menuntut koordinasi antara load balancer dan sistem service discovery Anda. Ia harus bekerja dengan sistem health check Anda dan pendaftaran/pencabutan pendaftaran di service discovery Anda.

Ini agak lanjutan: service discovery berarti jika Anda men-deploy seperangkat aplikasi (backend Anda, database Anda, instance Elasticsearch Anda), service discovery adalah mekanisme yang bertanggung jawab bagaimana mereka menemukan, menghubungkan, dan berkomunikasi satu sama lain setelah deployment. Saat shutdown, instance Anda perlu mencabut pendaftarannya supaya load balancer berhenti merutekan lalu lintas baru ke sana.


06 / STEP TWO

Pembersihan Resource

Langkah penting kedua. Anggap bekerja di meja Anda: saat waktunya meninggalkan rumah atau tidur, Anda melakukan sedikit pembersihan dulu. Jika tadi minum kopi, Anda membawa cangkir ke wastafel; Anda merapikan kabel, dll. Kita semua punya tugas pembersihan kecil sebelum meninggalkan meja. Hal yang sama berlaku untuk backend Anda.

Apa yang Dihitung sebagai "Resource"?

Saat kita bilang resource, maksudnya hal yang diperoleh aplikasi selama eksekusinya yang kini harus ia lepaskan:

  • File handle
  • Koneksi jaringan
  • Koneksi database
  • File sementara
  • Cache
  • Resource sistem lainnya

File Handle

Saat backend Anda berjalan dan mencoba mengakses lokasi tertentu di file system, cara kerjanya: Anda mengirim signal ke sistem operasi, dan ia memberi Anda handle ke file itu. Ada protokol cara proses mengakses file system yang mendasarinya, tapi di tingkat tinggi, Anda mendapat handle yang harus Anda lepaskan / bersihkan pada satu titik.

Why Unclosed Handles Are Dangerous

Jika Anda tidak membersihkan file handle, handle itu terus berjalan dan Anda akan mengakuisisi semakin banyak memori, artinya Anda terus memakan RAM (random access memory) Anda, dan pada satu titik Anda akan kehabisan. Sistem operasi juga membatasi jumlah file handle (dan koneksi jaringan) yang bisa dibuka proses tunggal secara bersamaan, jadi membocorkannya akhirnya memecah aplikasi Anda.

Koneksi Jaringan (Pembersihan Paling Umum)

Jenis pembersihan resource paling umum adalah membersihkan koneksi jaringan. Sistem operasi Anda perantara: semua request dari internet melewati OS Anda sebelum mencapai aplikasi Anda. OS adalah pengemudi sebenarnya yang menerima semua request dari network card Anda dan meneruskannya ke Anda, jadi ia punya pengetahuan penuh atas semua koneksi jaringan Anda.

OS as mediator between internet and application Internet incoming requests Operating System mediator / driver Your Backend the application
Fig 4.1: All network traffic passes through the OS before reaching your application

Sama seperti file handle, jika Anda tidak melepaskan koneksi jaringan setelah menanganinya, Anda akhirnya kehabisan memori atau menghadapi masalah performa, karena OS membatasi berapa banyak koneksi yang bisa dipegang terbuka sebuah proses.

Koneksi Database: Commit atau Rollback

Ini bagian yang langsung terikat dengan skenario pembayaran kita sejak awal. Sebelum proses aplikasi/backend Anda mati, transaksi database yang ia tangani harus di-commit atau di-rollback secara eksplisit oleh aplikasi Anda.

What Happens If You Don't

Jika Anda tidak commit atau rollback transaksi terbuka, transaksi itu bisa masuk ke state yang tidak konsisten, yang bisa berujung: deadlock, korupsi data, dan segala jenis masalah lain. Ini persis masalah tagihan-ganda / pembayaran-hilang dari skenario pembuka, diselesaikan dengan penanganan transaksi eksplisit saat shutdown.

Bersihkan dalam Urutan Terbalik dari Akuisisi

Satu aturan penting: saat membersihkan resource selama graceful shutdown, Anda ingin membersihkannya dalam urutan terbalik dari cara Anda memperolehnya, Last In, First Out (LIFO), seperti stack.

Reverse order resource cleanup LIFO ACQUIRE (in order) 1. Redis connection 2. DB connection 3. HTTP server shutdown reverses it RELEASE (reverse) 1. HTTP server 2. DB connection 3. Redis connection
Fig 4.2: Acquire Redis -> DB -> HTTP, but release in reverse: HTTP -> DB -> Redis (LIFO)

Misalnya, anggap Anda membuat koneksi Redis, lalu koneksi DB, lalu memulai server HTTP. Saat melepaskan resource, lakukan terbalik. Kenapa? Untuk mencegah situasi di mana Anda membersihkan resource atau operasi yang bergantung pada operasi sebelumnya. Merobohkan secara terbalik memastikan dependensi masih hidup saat hal yang membutuhkannya sedang dimatikan.


07 / CODE

Contoh Kode

Kita biasanya menghindari melihat kode di seri ini, tapi contoh praktis membantu menghindari pemahaman yang kosong. Anda tidak harus memahami setiap baris, cukup ikuti narasinya. Dalam praktik, kebanyakan framework (Node.js, Go, Rust, Python) menyediakan kode ini siap salin-tempel; yang penting memahami apa yang terjadi dan kenapa.

7.1: Graceful Shutdown di Go

Ini mencerminkan demo langsung dari kuliah: daftarkan handler yang menunggu signal, lalu saat menerimanya jalankan fungsi shutdown yang menutup server HTTP, lalu database, lalu server background job berbasis Redis, dalam urutan terbalik dari akuisisi.

main.go / Go / net/http + signal.NotifyContext
package main

import (
    "context"
    "errors"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // --- Startup phase: acquire resources in order ---
    db := connectDatabase()           // 1. acquire DB (TCP pool)
    jobs := startBackgroundJobs()     // 2. acquire Redis-backed worker

    srv := &http.Server{Addr: ":8080", Handler: router()}

    // Register a handler that waits for SIGINT (Ctrl+C) or SIGTERM (PM2/k8s).
    // We handle BOTH the same way, the intention is identical: shut down.
    ctx, stop := signal.NotifyContext(context.Background(),
        os.Interrupt, syscall.SIGTERM)
    defer stop()

    // Run the server in a goroutine so main can wait for the signal.
    go func() {
        log.Println("server started, ready to accept requests")
        if err := srv.ListenAndServe(); err != nil &&
            !errors.Is(err, http.ErrServerClosed) {
            log.Fatalf("listen error: %v", err)
        }
    }()

    // Block here until a signal arrives (the "living" phase).
    <-ctx.Done()
    log.Println("signal received, starting graceful shutdown")

    // Hard limit: give in-flight work up to 30 seconds, then force stop.
    shutdownCtx, cancel := context.WithTimeout(
        context.Background(), 30*time.Second)
    defer cancel()

    gracefulShutdown(shutdownCtx, srv, db, jobs)
    log.Println("server exited properly")
}

// gracefulShutdown releases resources in REVERSE order of acquisition.
// Acquired: DB -> jobs -> HTTP server. Released: HTTP -> jobs -> DB.
func gracefulShutdown(
    ctx context.Context,
    srv *http.Server,
    db *Database,
    jobs *JobServer,
) {
    // 1. CONNECTION DRAINING: srv.Shutdown stops accepting NEW
    //    connections and waits for in-flight requests to finish
    //    (or until the 30s ctx deadline forces it).
    log.Println("draining HTTP connections...")
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("forced HTTP shutdown: %v", err)
    }

    // 2. Stop the background job server (closes Redis connections,
    //    waits for workers to finish current jobs).
    log.Println("stopping background job server...")
    jobs.Shutdown()

    // 3. Close the database LAST, finish/commit open transactions,
    //    then close all pooled TCP connections one by one.
    log.Println("closing database connection...")
    if err := db.Close(); err != nil {
        log.Printf("db close error: %v", err)
    }
}

7.2: Graceful Shutdown di Python

Konsep yang sama di Python memakai modul signal: daftarkan handler untuk SIGINT dan SIGTERM, drain kerja in-flight, lalu lepaskan resource secara terbalik.

server.py / Python / signal + asyncio
import asyncio
import signal
import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger("app")

SHUTDOWN_TIMEOUT = 30  # hard limit in seconds


class Application:
    def __init__(self):
        self._shutdown = asyncio.Event()

    async def startup(self):
        # Acquire resources IN ORDER
        self.redis = await connect_redis()       # 1
        self.db = await connect_database()      # 2
        self.server = await start_http_server()  # 3
        log.info("server started, ready to accept requests")

    def install_signal_handlers(self):
        # Handle SIGINT (Ctrl+C, dev) and SIGTERM (PM2/k8s, prod)
        # the SAME way, both mean "shut down gracefully".
        loop = asyncio.get_running_loop()
        for sig in (signal.SIGINT, signal.SIGTERM):
            loop.add_signal_handler(sig, self._on_signal, sig)

    def _on_signal(self, sig):
        # NOTE: SIGKILL can never reach here, it cannot be
        # caught or ignored. Only the polite signals arrive.
        log.info(f"signal received: {sig.name}, shutting down")
        self._shutdown.set()

    async def graceful_shutdown(self):
        # 1. CONNECTION DRAINING: stop accepting new requests,
        #    let in-flight requests finish within the timeout.
        log.info("draining HTTP connections...")
        self.server.stop_accepting_new()
        try:
            await asyncio.wait_for(
                self.server.wait_for_inflight(),
                timeout=SHUTDOWN_TIMEOUT,
            )
        except asyncio.TimeoutError:
            log.warning("timeout exceeded, forcing shutdown")

        # 2 & 3. Release resources in REVERSE order of acquisition.
        #    Acquired redis -> db -> server; release server -> db -> redis.
        log.info("committing/rolling back open transactions...")
        await self.db.close()     # commit or rollback, then close pool
        log.info("closing redis connection...")
        await self.redis.close()
        log.info("server exited properly")


async def main():
    app = Application()
    await app.startup()
    app.install_signal_handlers()
    await app._shutdown.wait()   # the "living" phase, block until signal
    await app.graceful_shutdown()


if __name__ == "__main__":
    asyncio.run(main())

REFERENCES

Bacaan Lanjutan & Dokumentasi

Unix Signal

Go

Python & Node.js

Orkestrasi & Process Manager

MDN Web Docs

BACKEND ENGINEERING FIELD MANUAL / V2 / BAB 14 / GRACEFUL SHUTDOWN. Catatan disusun dari transkrip kuliah / contoh Go + Python / referensi Unix signals & orkestrasi inline